package com.fast.fast.common.redis.util;

import cn.hutool.core.util.StrUtil;
import com.fast.fast.common.redis.config.JedisConfig;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

/**
 * 基于Jedis API的分布式锁(集群时同一时刻只能有一个线程持有锁去执行锁之后的代码)
 * <p>
 * 分布式锁一般有如下的特点:
 * 互斥性：    同一时刻只能有一个节点的一个线程持有锁
 * 唯一性:     只能给自己上的锁解锁,不能给别人上的锁解锁
 * 防止死锁：  支持锁失效，加过期时间,防止死锁
 * 可重入性：  同一节点上的同一个线程如果获取了锁之后,自己依然能够再次获取锁
 * 高性能和高可用： 加锁和解锁要保证高性能和高可用(redis方案)
 * 具备重试机制：能够不切换线程,保持循环竞争的阻塞状态,并能够及时在阻塞状态中唤醒
 *
 * @author lyf
 * @date 2022/01/01 00:00 周六
 **/
@Slf4j
@RequiredArgsConstructor
@ConditionalOnBean(JedisConfig.class)
public class JedisDistributedLockUtils {

    private final Jedis jedis;

    /**
     * redis锁的key前缀
     */
    private static final String PREFIX = "lock-";

    /**
     * redis锁超时时间，防止死锁，单位秒
     */
    private static final long TIMEOUT = 10;

    /**
     * 使用jedis执行redis命令返回成功的结果
     */
    private static final String LOCK_SUCCESS = "OK";

    /*
      设置value的方式
      NX：当key不存在时才设置value，成功返回OK，失败返回(null)
      XX：当key存在时才设置value，成功返回OK，失败返回(null)
     */

    /*
      key的过期时间单位
      EX:秒
      PX:毫秒
     */

    /**
     * 基于redis的分布式锁--jedis方式
     * 原理:
     * 如果某个key不存在,set时会返回"OK",就认为某线程获取到了锁,可以执行之后的代码
     * 如果某个key存在,set时会返回null,就认为其它线程已经持有了锁,不能执行之后的代码
     * jedis.set(String key, String value, String nx|xx, String ex|px, int time)
     * 原生redis命令:set key value [NX|XX][EX seconds][PX milliseconds]
     *
     * @param key        键
     * @param requestId  请求的唯一编号
     * @param expireTime 过期时间,单位秒
     * @return 是否成功
     */
    @SneakyThrows
    public boolean distributedLock(String key, String requestId, Long expireTime) {
        if (StrUtil.isBlank(key) || StrUtil.isBlank(requestId)) {
            return false;
        }
        SetParams setParams = new SetParams().nx().ex(expireTime == null ? TIMEOUT : expireTime);
        log.debug("正在设置redis锁...");
        // 当前重试次数
        int start = 1;
        // 最大重试次数
        int retried = 3;
        // 使用while循环，加入重试机制，能够不切换线程，保持循环竞争的阻塞状态
        while (true) {
            // 判断依据:当key不存在时,是否能set成功
            String result = jedis.set(PREFIX + key, requestId, setParams);
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            // 休眠时间
            int sleepTime = 500;
            log.debug("设置redis锁失败,{}毫秒后自动重试第{}次", sleepTime, start);
            Thread.sleep(sleepTime);
            start++;
            if (start > retried) {
                log.debug("设置redis锁失败,已达最大重试次数");
                return false;
            }
        }
    }

    /**
     * 基于redis的分布式锁--jedis方式
     *
     * @param key       键
     * @param requestId 请求的唯一编号
     * @return 是否成功
     */
    public boolean distributedLock(String key, String requestId) {
        return this.distributedLock(key, requestId, TIMEOUT);
    }

}